Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / core / service / src / androidHostTest / kotlin / org / meshtastic / core / service / ConversationShortcutPublisherTest.kt

Displaying Raw • Download

core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.kt fix/qr-error-correction (7b45f84f) Text, 7.92 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.service

Tff7b72import T7ee787android.content.Context
Tff7b72import T7ee787android.content.pm.ShortcutManager
Tff7b72import T7ee787androidx.test.core.app.ApplicationProvider
Tff7b72import T7ee787androidx.test.ext.junit.runners.AndroidJUnit4
Tff7b72import T7ee787dev.mokkery.MockMode
Tff7b72import T7ee787dev.mokkery.answering.returns
Tff7b72import T7ee787dev.mokkery.every
Tff7b72import T7ee787dev.mokkery.mock
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.flowOf
Tff7b72import T7ee787kotlinx.coroutines.test.advanceUntilIdle
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787org.junit.Before
Tff7b72import T7ee787org.junit.Test
Tff7b72import T7ee787org.junit.runner.RunWith
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.DataPacket
Tff7b72import T7ee787org.meshtastic.core.model.Node
Tff7b72import T7ee787org.meshtastic.core.repository.NodeRepository
Tff7b72import T7ee787org.meshtastic.core.repository.PacketRepository
Tff7b72import T7ee787org.meshtastic.core.repository.RadioConfigRepository
Tff7b72import T7ee787org.meshtastic.proto.ChannelSet
Tff7b72import T7ee787org.meshtastic.proto.ChannelSettings
Tff7b72import T7ee787org.meshtastic.proto.User
Tff7b72import T7ee787org.robolectric.annotation.Config
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue
Tff7b72import T7ee787org.meshtastic.core.model.Channel Tff7b72as Te6edf3MeshChannel

T8b949e/**
* Behavioral coverage for conversation-shortcut publishing: recency ranking across DMs and channels, effective channel
* naming (empty primary → modem-preset name), and stale-shortcut removal.
*/
Tf0883e@RunWithTb4b4b4(Te6edf3AndroidJUnit4Tff7b72::Te6edf3classTb4b4b4)
Tf0883e@ConfigTb4b4b4(Te6edf3sdk Tff7b72= Tff7b72[T79c0ff3T79c0ff4Tff7b72]Tb4b4b4)
Tff7b72class T56d364ConversationShortcutPublisherTest Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3contextTb4b4b4: Te6edf3Context Tff7b72= Te6edf3ApplicationProviderTb4b4b4.Te6edf3getApplicationContextTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3shortcutManager Tff7b72= Te6edf3contextTb4b4b4.Te6edf3getSystemServiceTb4b4b4(Te6edf3ShortcutManagerTff7b72::Te6edf3classTb4b4b4.Te6edf3javaTb4b4b4)Tff7b72!!

Tff7b72private Tff7b72val Te6edf3hawk Tff7b72= Te6edf3NodeTb4b4b4(Te6edf3num Tff7b72= T79c0ff7Tb4b4b4, Te6edf3user Tff7b72= Te6edf3UserTb4b4b4(Te6edf3id Tff7b72= Ta5d6ff"Ta5d6ff!00000007Ta5d6ff"Tb4b4b4, Te6edf3long_name Tff7b72= Ta5d6ff"Ta5d6ffHawk RidgeTa5d6ff"Tb4b4b4, Te6edf3short_name Tff7b72= Ta5d6ff"Ta5d6ffHAWKTa5d6ff"Tb4b4b4)Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3packetRepositoryTb4b4b4: Te6edf3PacketRepository Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3nodeRepositoryTb4b4b4: Te6edf3NodeRepository Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3radioConfigRepositoryTb4b4b4: Te6edf3RadioConfigRepository Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3dispatchers Tff7b72=
Te6edf3CoroutineDispatchersTb4b4b4(
Te6edf3io Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Te6edf3main Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Te6edf3default Tff7b72= Te6edf3DispatchersTb4b4b4.Te6edf3UnconfinedTb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3publisher Tff7b72=
Te6edf3ConversationShortcutPublisherTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3nodeRepositoryTb4b4b4, Te6edf3packetRepositoryTb4b4b4, Te6edf3radioConfigRepositoryTb4b4b4, Te6edf3dispatchersTb4b4b4)

Tf0883e@Before
Tff7b72fun Td2a8ffsetUpTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3shortcutManagerTb4b4b4.Te6edf3removeAllDynamicShortcutsTb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeRepositoryTb4b4b4.Te6edf3nodeDBbyNum Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Te6edf3mapOfTb4b4b4(T79c0ff7 Te6edf3to Te6edf3hawkTb4b4b4)Tb4b4b4)
T8b949e// Primary channel with an empty name (resolves to the preset name) plus one named secondary channel.
Te6edf3every Tb4b4b4{ Te6edf3radioConfigRepositoryTb4b4b4.Te6edf3channelSetFlow Tb4b4b4} Te6edf3returns
Te6edf3flowOfTb4b4b4(
Te6edf3ChannelSetTb4b4b4(
Te6edf3settings Tff7b72= Te6edf3listOfTb4b4b4(Te6edf3MeshChannelTb4b4b4.Te6edf3defaultTb4b4b4.Te6edf3settingsTb4b4b4, Te6edf3ChannelSettingsTb4b4b4(Te6edf3name Tff7b72= Ta5d6ff"Ta5d6ffBetaTa5d6ff"Tb4b4b4)Tb4b4b4)Tb4b4b4,
Te6edf3lora_config Tff7b72= Te6edf3MeshChannelTb4b4b4.Te6edf3defaultTb4b4b4.Te6edf3loraConfigTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffcontactTb4b4b4(Te6edf3fromTb4b4b4: Tffa657String?Tb4b4b4, Te6edf3timeTb4b4b4: Tffa657LongTb4b4b4) Tff7b72= Te6edf3DataPacketTb4b4b4(Te6edf3bytes Tff7b72= Tff7b72nullTb4b4b4, Te6edf3dataType Tff7b72= T79c0ff1Tb4b4b4, Te6edf3from Tff7b72= Te6edf3fromTb4b4b4, Te6edf3time Tff7b72= Te6edf3timeTb4b4b4)

Tf0883e@Test
Tff7b72fun Td2a8ff`shortcuts are ranked by recency across dms and channels`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3packetRepositoryTb4b4b4.Te6edf3getContactsTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns
Te6edf3flowOfTb4b4b4(
Te6edf3mapOfTb4b4b4(
Ta5d6ff"Ta5d6ff0!00000007Ta5d6ff" Te6edf3to Te6edf3contactTb4b4b4(Te6edf3from Tff7b72= Ta5d6ff"Ta5d6ff!00000007Ta5d6ff"Tb4b4b4, Te6edf3time Tff7b72= T79c0ff3Te6edf3_000Tb4b4b4)Tb4b4b4, T8b949e// newest → rank 0
Ta5d6ff"Ta5d6ff0^allTa5d6ff" Te6edf3to Te6edf3contactTb4b4b4(Te6edf3from Tff7b72= Tff7b72nullTb4b4b4, Te6edf3time Tff7b72= T79c0ff2Te6edf3_000Tb4b4b4)Tb4b4b4, T8b949e// rank 1; "Beta" never messaged → last
Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Te6edf3publisherTb4b4b4.Te6edf3startObservingTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3ranks Tff7b72= Te6edf3shortcutManagerTb4b4b4.Te6edf3dynamicShortcutsTb4b4b4.Te6edf3associate Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Te6edf3to Tffa657itTb4b4b4.Te6edf3rank Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3ranksTff7b72[Ta5d6ff"Ta5d6ff0!00000007Ta5d6ff"Tff7b72]Tb4b4b4, Ta5d6ff"Ta5d6ffmost recently active conversation ranks firstTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff1Tb4b4b4, Te6edf3ranksTff7b72[Ta5d6ff"Ta5d6ff0^allTa5d6ff"Tff7b72]Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff2Tb4b4b4, Te6edf3ranksTff7b72[Ta5d6ff"Ta5d6ff1^allTa5d6ff"Tff7b72]Tb4b4b4, Ta5d6ff"Ta5d6ffnever-messaged channel sorts lastTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`dm shortcuts carry node labels and the empty primary resolves to its preset name`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3packetRepositoryTb4b4b4.Te6edf3getContactsTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns
Te6edf3flowOfTb4b4b4(Te6edf3mapOfTb4b4b4(Ta5d6ff"Ta5d6ff0!00000007Ta5d6ff" Te6edf3to Te6edf3contactTb4b4b4(Te6edf3from Tff7b72= Ta5d6ff"Ta5d6ff!00000007Ta5d6ff"Tb4b4b4, Te6edf3time Tff7b72= T79c0ff1Te6edf3_000Tb4b4b4)Tb4b4b4)Tb4b4b4)

Te6edf3publisherTb4b4b4.Te6edf3startObservingTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3byId Tff7b72= Te6edf3shortcutManagerTb4b4b4.Te6edf3dynamicShortcutsTb4b4b4.Te6edf3associateBy Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffHAWKTa5d6ff"Tb4b4b4, Te6edf3byIdTb4b4b4.Te6edf3getValueTb4b4b4(Ta5d6ff"Ta5d6ff0!00000007Ta5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3shortLabelTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffHawk RidgeTa5d6ff"Tb4b4b4, Te6edf3byIdTb4b4b4.Te6edf3getValueTb4b4b4(Ta5d6ff"Ta5d6ff0!00000007Ta5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3longLabelTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffLongFastTa5d6ff"Tb4b4b4, Te6edf3byIdTb4b4b4.Te6edf3getValueTb4b4b4(Ta5d6ff"Ta5d6ff0^allTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3shortLabelTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffBetaTa5d6ff"Tb4b4b4, Te6edf3byIdTb4b4b4.Te6edf3getValueTb4b4b4(Ta5d6ff"Ta5d6ff1^allTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3shortLabelTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`stale shortcuts are removed when no longer part of the conversation set`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// Simulate a leftover shortcut from a previous session (raw push: the in-memory on-demand protection does not
T8b949e// survive a process restart, so the observer owns its cleanup).
Te6edf3pushRawShortcutTb4b4b4(Ta5d6ff"Ta5d6ff9^allTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffOld ChannelTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3shortcutManagerTb4b4b4.Te6edf3dynamicShortcutsTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ff9^allTa5d6ff" Tb4b4b4}Tb4b4b4)

Te6edf3every Tb4b4b4{ Te6edf3packetRepositoryTb4b4b4.Te6edf3getContactsTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3flowOfTb4b4b4(Te6edf3emptyMapTb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3publisherTb4b4b4.Te6edf3startObservingTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertNullTb4b4b4(Te6edf3shortcutManagerTb4b4b4.Te6edf3dynamicShortcutsTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ff9^allTa5d6ff" Tb4b4b4}Tb4b4b4, Ta5d6ff"Ta5d6ffstale shortcut should be prunedTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3shortcutManagerTb4b4b4.Te6edf3dynamicShortcutsTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ff0^allTa5d6ff" Tb4b4b4}Tb4b4b4, Ta5d6ff"Ta5d6ffcurrent channels are publishedTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`on-demand shortcuts survive pruning until a snapshot includes their conversation`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// A brand-new conversation's first notification publishes its shortcut before the contacts flow emits it.
Te6edf3publisherTb4b4b4.Te6edf3ensureConversationShortcutTb4b4b4(Ta5d6ff"Ta5d6ff0!000000ffTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffNew PeerTa5d6ff"Tb4b4b4)

Te6edf3every Tb4b4b4{ Te6edf3packetRepositoryTb4b4b4.Te6edf3getContactsTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3returns Te6edf3flowOfTb4b4b4(Te6edf3emptyMapTb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3publisherTb4b4b4.Te6edf3startObservingTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3advanceUntilIdleTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(
Te6edf3shortcutManagerTb4b4b4.Te6edf3dynamicShortcutsTb4b4b4.Te6edf3any Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ff0!000000ffTa5d6ff" Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffpending on-demand shortcut must not be pruned before the snapshot catches upTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`on-demand shortcut with a blank display name falls back instead of throwing`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// Regression: a reaction from a node the NodeDB does not know yet supplied an empty name, and
T8b949e// ShortcutInfoCompat.Builder.build() threw "Shortcut must have a non-empty label" out of the caller's
T8b949e// coroutine, dropping the notification (2.8.0 build 29321664).
Te6edf3publisherTb4b4b4.Te6edf3ensureConversationShortcutTb4b4b4(Ta5d6ff"Ta5d6ff0!000000feTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff"Tb4b4b4)

Tff7b72val Te6edf3published Tff7b72= Te6edf3shortcutManagerTb4b4b4.Te6edf3dynamicShortcutsTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ff0!000000feTa5d6ff" Tb4b4b4}
Te6edf3assertNotNullTb4b4b4(Te6edf3publishedTb4b4b4, Ta5d6ff"Ta5d6ffa blank display name must still publish a shortcutTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3publishedTb4b4b4.Te6edf3shortLabelTff7b72?.Te6edf3isNotBlankTb4b4b4(Tb4b4b4) Tff7b72=Tff7b72= Tff7b72trueTb4b4b4, Ta5d6ff"Ta5d6ffshort label must be non-blankTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffpushRawShortcutTb4b4b4(Te6edf3idTb4b4b4: Tffa657StringTb4b4b4, Te6edf3labelTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3shortcut Tff7b72=
Te6edf3androidxTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3contentTb4b4b4.Te6edf3pmTb4b4b4.Te6edf3ShortcutInfoCompatTb4b4b4.Te6edf3BuilderTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3idTb4b4b4)
Tb4b4b4.Te6edf3setShortLabelTb4b4b4(Te6edf3labelTb4b4b4)
Tb4b4b4.Te6edf3setIntentTb4b4b4(Te6edf3androidTb4b4b4.Te6edf3contentTb4b4b4.Te6edf3IntentTb4b4b4(Te6edf3androidTb4b4b4.Te6edf3contentTb4b4b4.Te6edf3IntentTb4b4b4.Te6edf3ACTION_VIEWTb4b4b4)Tb4b4b4)
Tb4b4b4.Te6edf3buildTb4b4b4(Tb4b4b4)
Te6edf3androidxTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3contentTb4b4b4.Te6edf3pmTb4b4b4.Te6edf3ShortcutManagerCompatTb4b4b4.Te6edf3pushDynamicShortcutTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3shortcutTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.1s